package com.androwit.engine.mapping.auto;

import java.beans.PropertyDescriptor;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;

import org.apache.commons.beanutils.BeanUtilsBean;

import com.androwit.engine.mapping.BasicMappingField;
import com.androwit.engine.mapping.Converter;
import com.androwit.engine.mapping.MappingField;
import com.androwit.engine.mapping.MappingFieldsDefiner;

/**
 * The default behavior of this mapper is the expected one : <br/>
 * 
 * Fields are mapped directly with null converters (no conversion) and with the
 * same name on both sides. In addition, fields bean properties must be readable
 * and writable to be added. You can override
 * <ul>
 * <li>{@link #createMappingField(PropertyDescriptor)} to control the entire
 * mapping creation behavior. This method uses the three following methods to
 * construct its result</li>
 * <li>{@link #getConverter(PropertyDescriptor)} to use your converter in the
 * default mapping</li>
 * <li>{@link #getKey(PropertyDescriptor)} to use a different key in the default
 * mapping</li>
 * <li>{@link #getName(PropertyDescriptor)} to use a different target name in
 * the default mapping</li>
 * 
 * <br/>
 * If a field is declared transient, it will not be in the mapping. You can
 * control which fields will be in the mapping by overriding
 * {@link #shoudAddAccessor(PropertyDescriptor)}. PropertyDescriptor is the
 * apache common BeanUtils object to access property info.
 * 
 * @author Zied Hamdi http://1vu.fr
 * 
 * @param <T>
 *          the class it represents type
 */
public class ClassReflexionFieldsDefiner<T> implements
		MappingFieldsDefiner<Object, String, Object> {
	protected Class<T> clazz;
	protected List<MappingField<Object, String, Object>> fields = new ArrayList<MappingField<Object, String, Object>>();

	public ClassReflexionFieldsDefiner(Class<T> clazz) {
		this.clazz = clazz;
		constructFields();
	}

	/**
	 * Constructs and keeps in the current instance the mapping fields for the
	 * current class
	 */
	protected void constructFields() {
		PropertyDescriptor[] propertyDescriptors = BeanUtilsBean.getInstance()
				.getPropertyUtils().getPropertyDescriptors(clazz);
		for (PropertyDescriptor propertyDescriptor : propertyDescriptors) {
			if (shoudAddAccessor(propertyDescriptor)) {
				MappingField<Object, String, Object> basicMappingField = createMappingField(propertyDescriptor);
				addMappingField(basicMappingField);
			}
		}
	}

	/**
	 * Override to define your strategy to decide whether a field should be added
	 * (eg. using an annotation)
	 * 
	 * @param property
	 *          the field name
	 * @param analyzedClass
	 *          the current class info
	 * @return true if this field should be included in mapping
	 */
	protected boolean shoudAddAccessor(PropertyDescriptor propertyDescriptor) {
		if (propertyDescriptor.getWriteMethod() == null)
			return false;
		Enumeration<String> attributeNames = propertyDescriptor.attributeNames();
		boolean tstInCache = false;
		while (attributeNames.hasMoreElements()) {
			String attribute = attributeNames.nextElement();
			if ("transient".equals(attribute)) {
				tstInCache = true;
				Object tst = propertyDescriptor.getValue(attribute);
				if (tst != null && (Boolean) tst)
					return false;
			}
		}
		String name = propertyDescriptor.getName();
		try {
			// if the object is analyzed for the first time put metadata
			if (!tstInCache) {
				Field field = findFieldInClassOrParents(clazz, name);
				if (field != null) {
					boolean isNotTst = (field.getModifiers() & Modifier.TRANSIENT) == 0;
					propertyDescriptor.setValue("transient", isNotTst);
					if (!isNotTst) {
						return false;
					}
				} else
					; // Cannot find field 'name', the accessors without field are
						// considered valid

			}
		} catch (SecurityException ex) {
			ex.printStackTrace();
		} catch (NoSuchFieldException ex) {
			ex.printStackTrace();
		}
		// System.out.println("Attribute names for " + name + " : " +
		// attributeNames);

		return true;
	}

	/**
	 * utility method to find a field in the object hierarchy.
	 * 
	 * @param clazzToSearchIn
	 * @param fieldName
	 * @return
	 * @throws NoSuchFieldException
	 */
	protected Field findFieldInClassOrParents(Class clazzToSearchIn,
			String fieldName) throws NoSuchFieldException {
		Field[] fieldList = clazzToSearchIn.getDeclaredFields();
		for (Field field : fieldList) {
			if (field.getName().equals(fieldName))
				return field;
		}
		Class superclass = clazzToSearchIn.getSuperclass();
		if (superclass != null)
			return findFieldInClassOrParents(superclass, fieldName);

		return null;
	}

	/**
	 * Creates the mapping field for the property <code>propertyDescriptor</code>
	 * 
	 * @param propertyDescriptor
	 *          the property to construct a field for
	 * @return the mapping, null is not allowed: use
	 *         {@link #shoudAddAccessor(PropertyDescriptor)} to skip a property
	 */
	protected MappingField<Object, String, Object> createMappingField(
			PropertyDescriptor propertyDescriptor) {
		return new BasicMappingField<Object, String, Object>(
				getKey(propertyDescriptor), getConverter(propertyDescriptor),
				getName(propertyDescriptor));
	}

	protected void addMappingField(
			MappingField<Object, String, Object> basicMappingField) {
		fields.add(basicMappingField);
	}

	/**
	 * by overriding this method you can decide on the convertuer to use
	 * 
	 * @param propertyDescriptor
	 * @return
	 */
	protected Converter<Object, Object> getConverter(
			PropertyDescriptor propertyDescriptor) {
		return null;
	}

	/**
	 * You can control renaming of fields in source here
	 * 
	 * @param propertyDescriptor
	 * 
	 * @return
	 */
	protected String getKey(PropertyDescriptor propertyDescriptor) {
		return propertyDescriptor.getName();
	}

	/**
	 * You can control renaming of fields in target here
	 * 
	 * @param property
	 * @return
	 */
	protected String getName(PropertyDescriptor propertyDescriptor) {
		return propertyDescriptor.getName();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * com.androwit.engine.mapping.MappingFieldsDefiner#getMappedFields(java.util
	 * .Map)
	 */
	@SuppressWarnings("unchecked")
	@Override
	public MappingField<Object, String, Object>[] getMappedFields(
			Map<String, Object> values) {
		return fields.toArray(new MappingField[0]);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * com.androwit.engine.mapping.MappingFieldsDefiner#createInstance(java.util
	 * .Map)
	 */
	@Override
	public Object createInstance(Map<String, Object> values) {
		try {
			return clazz.newInstance();
		} catch (Exception ex) {
			throw new RuntimeException(ex);
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * com.androwit.engine.mapping.MappingFieldsDefiner#getMappingFields(java.
	 * lang.Object)
	 */
	@SuppressWarnings("unchecked")
	@Override
	public MappingField<Object, String, Object>[] getMappingFields(
			Object toPersist) {
		return fields.toArray(new MappingField[0]);
	}

}
